ENTSO-E Electricity Data Analysis¶
Imports¶
In [1]:
import numpy as np
import plotly.graph_objects as go
import plotly.express as px
import pandas as pd
import sys
sys.path.insert(1,'../')
from scripts.plotting_utils import *
Load Processed Data¶
In [2]:
df = pd.read_feather("../data/processed_data.feather")
df.DE_LU.demand
Out[2]:
2023-12-31 23:00:00+00:00 39981.75
2024-01-01 00:00:00+00:00 38085.75
2024-01-01 01:00:00+00:00 37063.50
2024-01-01 02:00:00+00:00 36834.25
2024-01-01 03:00:00+00:00 36835.25
...
2025-08-18 18:00:00+00:00 54441.00
2025-08-18 19:00:00+00:00 52577.75
2025-08-18 20:00:00+00:00 49661.00
2025-08-18 21:00:00+00:00 46344.50
2025-08-18 22:00:00+00:00 46344.50
Name: demand, Length: 14304, dtype: float64
In [3]:
df.DE_LU.index
Out[3]:
DatetimeIndex(['2023-12-31 23:00:00+00:00', '2024-01-01 00:00:00+00:00',
'2024-01-01 01:00:00+00:00', '2024-01-01 02:00:00+00:00',
'2024-01-01 03:00:00+00:00', '2024-01-01 04:00:00+00:00',
'2024-01-01 05:00:00+00:00', '2024-01-01 06:00:00+00:00',
'2024-01-01 07:00:00+00:00', '2024-01-01 08:00:00+00:00',
...
'2025-08-18 13:00:00+00:00', '2025-08-18 14:00:00+00:00',
'2025-08-18 15:00:00+00:00', '2025-08-18 16:00:00+00:00',
'2025-08-18 17:00:00+00:00', '2025-08-18 18:00:00+00:00',
'2025-08-18 19:00:00+00:00', '2025-08-18 20:00:00+00:00',
'2025-08-18 21:00:00+00:00', '2025-08-18 22:00:00+00:00'],
dtype='datetime64[ns, UTC]', length=14304, freq=None)
Analysis and Visualization¶
In [4]:
start = pd.Timestamp('20240630', tz='Europe/Brussels')
end = pd.Timestamp('20250630', tz='Europe/Brussels')
period = slice(start,end)
# df_FR = df.FR.loc[period]
df_FR = df.FR
data = []
data.append(
go.Scatter(
x=df_FR.index,
y=df_FR.demand,
line=dict(
width=0.5,
color=light_blue_gray,
),
showlegend=False,
)
)
data.append(
go.Scatter(
x=df_FR.index,
y=df_FR.demand.rolling(window=24 * 7*4, center=True, min_periods=24*7).mean(),
line=dict(
color=fca_blue,
),
name="1 month moving average",
)
)
In [5]:
layout = go.Layout(
template=fca_template,
title_text="Electricity consumption France (MW)",
yaxis_range=[0,89e3]
)
fig = go.Figure(data=data, layout=layout)
fig.show()
In [6]:
layout = go.Layout(
template=fca_template,
title_text="Electricity consumption over one week, France (MW)",
yaxis_range=[0,89e3],
xaxis_range=[
pd.Timestamp('2023-08-18 22:00:00'),
pd.Timestamp('2023-08-26 4:00:00')
]
)
fig = go.Figure(data=data, layout=layout)
fig.show()
In [7]:
px.density_heatmap(
df_FR,
x="hour",
y="demand",
# y="weighted_market_val",
nbinsy=60,
nbinsx=24,
color_continuous_scale=cm_data_plotly,
).update_layout(template=fca_template, title_text='Frequency of demand by hour, France (green-yellow = most frequent values)')
In [8]:
df_DE = df.DE_LU
# 0.109 is the capital recovery factor
avgs = []
for q in np.linspace(0,1,500):
quant = df_DE.price.quantile(q)
mean = df_DE.query('price <= @quant').price.mean()
# avgs.append(mean if mean >= 0 else 0)
avgs.append(mean)
avgs = np.array(avgs)
fig = go.Figure()
fig.add_trace(
go.Scatter(
x=np.linspace(0, 1, 500),
y=avgs / 1.0, # 100% efficiency, roughly cancels out with gas price being GCV ~50/55?
name="average Electricity cost 1MWh_heat",
line_color="black",
)
)
fig.add_trace(
go.Scatter(
x=np.linspace(0, 1, 500),
y=1000e3 * 0.109 / (365 * 24 * np.linspace(0, 1, 500)),
name="1000 EUR/kW",
line_color=blue_gray,
line_dash="dot",
)
)
# fig.add_trace(go.Scatter(x=np.linspace(0,1,100), y=500e3*0.109/(365*24*np.linspace(0,1,100)), name='500 EUR/kW' ))
fig.add_trace(
go.Scatter(
x=np.linspace(0, 1, 500),
y=500e3 * 0.109 / (365 * 24 * np.linspace(0, 1, 500)),
name="500 EUR/kW",
line_color=dark_gray,
line_dash="dot",
)
)
fig.add_trace(
go.Scatter(
x=np.linspace(0, 1, 500),
y=250e3 * 0.109 / (365 * 24 * np.linspace(0, 1, 500)),
name="250 EUR/kW",
line_color=fca_blue,
line_dash="dot",
)
)
fig.add_trace(
go.Scatter(
x=np.linspace(0, 1, 500),
y=100e3 * 0.109 / (365 * 24 * np.linspace(0, 1, 500)),
name="100 EUR/kW",
line_color=highlight_blue,
line_dash="dot",
)
)
fig.add_trace(
go.Scatter(
x=np.linspace(0, 1, 500),
y=avgs
+ 1000e3 * 0.109 / (365 * 24 * np.linspace(0, 1, 500)),
showlegend=False,
line_color=blue_gray,
)
)
fig.add_trace(
go.Scatter(
x=np.linspace(0, 1, 500),
y=avgs
+ 500e3 * 0.109 / (365 * 24 * np.linspace(0, 1, 500)),
name="LCoHeat",
line_color=dark_gray,
)
)
fig.add_trace(
go.Scatter(
x=np.linspace(0, 1, 500),
y=avgs
+ 250e3 * 0.109 / (365 * 24 * np.linspace(0, 1, 500)),
name="LCoHeat",
line_color=fca_blue,
)
)
fig.add_trace(
go.Scatter(
x=np.linspace(0, 1, 500),
y=avgs
+ 100e3 * 0.109 / (365 * 24 * np.linspace(0, 1, 500)),
showlegend=False,
line_color=highlight_blue,
)
)
# fig.add_trace(go.Scatter(x=np.linspace(0,1,500), y=df_DE.price.quantile(np.linspace(0,1,500)) + 500e3*0.109/(365*24*np.linspace(0,1,500)),name='LCoH'))
fig.update_layout(
dict(
width=800,
title="LCOHeat for Germany, 01/2023 - 12/2023<br> as a function of production at cheapest X percent of price hours",
xaxis_title="price hour percentile",
yaxis_title="eur/MWh",
)
)
fig.update_layout(template=fca_template, yaxis_range=[-25,110], xaxis_range=[0,1.19], legend=dict(x=1, y=0.6))
fig.show()
/var/folders/3q/5mkhpnj53p1gc9gp2z3f4tkm0000gn/T/ipykernel_62257/2248199203.py:24: RuntimeWarning: divide by zero encountered in divide /var/folders/3q/5mkhpnj53p1gc9gp2z3f4tkm0000gn/T/ipykernel_62257/2248199203.py:34: RuntimeWarning: divide by zero encountered in divide /var/folders/3q/5mkhpnj53p1gc9gp2z3f4tkm0000gn/T/ipykernel_62257/2248199203.py:43: RuntimeWarning: divide by zero encountered in divide /var/folders/3q/5mkhpnj53p1gc9gp2z3f4tkm0000gn/T/ipykernel_62257/2248199203.py:52: RuntimeWarning: divide by zero encountered in divide /var/folders/3q/5mkhpnj53p1gc9gp2z3f4tkm0000gn/T/ipykernel_62257/2248199203.py:62: RuntimeWarning: divide by zero encountered in divide /var/folders/3q/5mkhpnj53p1gc9gp2z3f4tkm0000gn/T/ipykernel_62257/2248199203.py:72: RuntimeWarning: divide by zero encountered in divide /var/folders/3q/5mkhpnj53p1gc9gp2z3f4tkm0000gn/T/ipykernel_62257/2248199203.py:81: RuntimeWarning: divide by zero encountered in divide /var/folders/3q/5mkhpnj53p1gc9gp2z3f4tkm0000gn/T/ipykernel_62257/2248199203.py:90: RuntimeWarning: divide by zero encountered in divide
fix me¶
In [9]:
df_EU = df
# # only 202X
y0 = 2022
y1 = 2023
# period = slice(f'{y0}-01-01 00:00:00',f'{y1}-01-01 00:00:00')
df_DE_2 = df_EU.DE_LU.copy()
y0, y1 = 2023, 2024
df23 = (df_DE_2
.copy()
.loc[slice(f'{y0}-01-01 00:00:00', f'{y1}-01-01 00:00:00')]
.assign(market_val = lambda df: df.price * df.vre / df.demand)
.assign(vre_quant = lambda df: df.vre/df.vre.max())
)
vre_mean = df23.vre.mean()
vre_quant = df23.vre.quantile(0.75)
bin_width = 1000
df_DE_2 = df23.copy()
df_DE_2_vre = df_DE_2.assign(vre_bin=lambda df: np.floor((df.vre / bin_width))*bin_width).groupby('vre_bin').price.describe()
df_DE_2_means = df_DE_2.assign(vre_bin=lambda df: np.floor((df.vre / bin_width))*bin_width).groupby('vre_bin').mean()
df_DE_2_median = df_DE_2.assign(vre_bin=lambda df: np.floor((df.vre / bin_width))*bin_width).groupby('vre_bin').median()
# df_DE_2_wind = df_DE_2.assign(wind_bin=lambda df: np.floor((df.wind / bin_width))*bin_width).groupby('wind_bin').mean()
df_DE_2_wind = df_DE_2.assign(wind_bin=lambda df: np.floor((df.wind / bin_width))*bin_width).groupby('wind_bin').price.describe()
# df_DE_2_wind['mean']
df_DE_2_wind.head()
# df_DE_2_solar = df_DE_2.assign(solar_bin=lambda df: np.floor((df.solar / bin_width))*bin_width).groupby('solar_bin').mean()
df_DE_2_solar = df_DE_2.assign(solar_bin=lambda df: np.floor((df.solar / bin_width))*bin_width).groupby('solar_bin').price.describe()
# df_DE_2_solar['mean']
df_DE_2_solar['25%']
fig = go.Figure()
scatter = go.Scatter(
x=df_DE_2.vre,
y=df_DE_2.market_val,
mode='markers',
marker=dict(opacity=0.1, size=3, color=gray),
name='Hourly market value (VRE production × day ahead price)',
# y="weighted_market_val",
# nbinsy=3000,
# nbinsx=1000,
# color_continuous_scale="magma",
)
fig.update_layout(
template=fca_template,
yaxis_range=[-0.34e6, 4.2e6],
xaxis_range=[-1000, 62e3],
yaxis_title_text='Market value (€)',
xaxis_title_text='VRE production (MW)',
title=f"Market value of VREs declining with rising share of generation in Germany from {y0} to {y1}",
legend=dict(
x=1.1,
y=1,
xanchor='left',
yanchor='top',
),
height=540*1.4,
width=960*1.4,
)
# fig.add_trace(scatter)
fig.add_trace(
go.Scatter(
x=[0, df_DE_2.vre.max()],
y=[0, df_DE_2.price.mean() * df_DE_2.vre.max()],
mode="lines",
line_color=very_dark_gray,
line_width=4,
name=f'VRE production × Mean price {y0}-{y1}'
)
)
fig.add_vline(x=df_DE_2.demand.mean(), annotation=dict(text='mean demand'))
green = 'rgba(145, 192, 150, 1)'
fig.add_trace(
go.Scatter(
x=df_DE_2_means.index,
y=df_DE_2_means.price*df_DE_2_means.index,
mode="lines",
line_color=green,
line_width=4,
name='Mean VRE market value'
)
)
green = 'rgba(145, 192, 150, 0.2)'
fig.add_traces(
go.Scatter(
x=df_DE_2_vre["25%"].index,
y=df_DE_2_vre["25%"] * df_DE_2_vre["25%"].index,
line=dict(color=green),
name='IQR',
)
)
fig.add_traces(
go.Scatter(
x=df_DE_2_vre["75%"].index,
y=df_DE_2_vre["75%"] * df_DE_2_vre["75%"].index,
fill="tonexty",
fillcolor=green,
line=dict(color=green),
showlegend=False,
)
)
sand_yellow = 'rgba(226, 182, 129, 1)'
fig.add_trace(
go.Scatter(
x=df_DE_2_solar.index,
y=df_DE_2_solar['mean']*df_DE_2_solar.index,
mode="lines",
line_color=sand_yellow,
line_width=4,
name='Mean solar market value'
)
)
sand_yellow = 'rgba(226, 182, 129, 0.2)'
fig.add_traces(
go.Scatter(
x=df_DE_2_solar["25%"].index,
y=df_DE_2_solar["25%"] * df_DE_2_solar["25%"].index,
line=dict(color=sand_yellow),
name='IQR',
)
)
fig.add_traces(
go.Scatter(
x=df_DE_2_solar["75%"].index,
y=df_DE_2_solar["75%"] * df_DE_2_solar["75%"].index,
fill="tonexty",
fillcolor=sand_yellow,
line=dict(color=sand_yellow),
showlegend=False,
)
)
highlight_blue = 'rgba(2, 147, 210, 1)'
fig.add_trace(
go.Scatter(
x=df_DE_2_wind.index,
y=df_DE_2_wind['mean']*df_DE_2_wind.index,
mode="lines",
line_color=highlight_blue,
line_width=4,
name='Mean wind market value'
)
)
highlight_blue = 'rgba(2, 147, 210, 0.2)'
fig.add_traces(
go.Scatter(
x=df_DE_2_wind["25%"].index,
y=df_DE_2_wind["25%"] * df_DE_2_wind["25%"].index,
line=dict(color=highlight_blue),
name='IQR',
)
)
fig.add_traces(
go.Scatter(
x=df_DE_2_wind["75%"].index,
y=df_DE_2_wind["75%"] * df_DE_2_wind["75%"].index,
fill="tonexty",
fillcolor=highlight_blue,
opacity=0.1,
line=dict(color="rgba(0,0,0,0)"),
showlegend=False,
)
)
fig.show()
In [ ]: